---
title: File System
description: Access the file system.
plugin: fs
---

import PluginLinks from '@components/PluginLinks.astro';
import Compatibility from '@components/plugins/Compatibility.astro';

import { Tabs, TabItem, Steps } from '@astrojs/starlight/components';
import CommandTabs from '@components/CommandTabs.astro';
import PluginPermissions from '@components/PluginPermissions.astro';

<PluginLinks plugin={frontmatter.plugin} />

Access the file system.

## Supported Platforms

<Compatibility plugin={frontmatter.plugin} />

## Setup

Install the fs plugin to get started.

<Tabs>
	<TabItem label="Automatic" >

    	Use your project's package manager to add the dependency:

    	{ ' ' }

    	<CommandTabs
            npm="npm run tauri add fs"
            yarn="yarn run tauri add fs"
            pnpm="pnpm tauri add fs"
            bun="bun tauri add fs"
            cargo="cargo tauri add fs"
    	/>

    </TabItem>
    <TabItem label = "Manual">
    	<Steps>

        1. Run the following command in the `src-tauri` folder to add the plugin to the project's dependencies in `Cargo.toml`:

            ```sh frame=none
            cargo add tauri-plugin-fs
            ```

        2.	Modify `lib.rs` to initialize the plugin:

            ```rust title="src-tauri/src/lib.rs" ins={4}
            #[cfg_attr(mobile, tauri::mobile_entry_point)]
            pub fn run() {
              tauri::Builder::default()
                .plugin(tauri_plugin_fs::init())
                .run(tauri::generate_context!())
                .expect("error while running tauri application");
            }
            ```

        3.	Install the JavaScript Guest bindings using your preferred JavaScript package manager:

            <CommandTabs
                npm = "npm install @tauri-apps/plugin-fs"
                yarn = "yarn add @tauri-apps/plugin-fs"
                pnpm = "pnpm add @tauri-apps/plugin-fs"
                bun = "bun add @tauri-apps/plugin-fs"
            />

    	</Steps>
    </TabItem>

</Tabs>

## Configuration

### Android

When using the audio, cache, documents, downloads, picture, public or video directories your app must have access to the external storage.

Include the following permissions to the `manifest` tag in the `gen/android/app/src/main/AndroidManifest.xml` file:

```xml
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"/>
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
```

### iOS

Apple requires app developers to specify approved reasons for API usage to enhance user privacy.

You must create a `PrivacyInfo.xcprivacy` file in the `src-tauri/gen/apple` folder
with the required [NSPrivacyAccessedAPICategoryFileTimestamp] key and the [C617.1] recommended reason.

```xml
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
  <dict>
    <key>NSPrivacyAccessedAPITypes</key>
    <array>
      <dict>
        <key>NSPrivacyAccessedAPIType</key>
        <string>NSPrivacyAccessedAPICategoryFileTimestamp</string>
        <key>NSPrivacyAccessedAPITypeReasons</key>
        <array>
          <string>C617.1</string>
        </array>
      </dict>
    </array>
  </dict>
</plist>
```

## Usage

The fs plugin is available in both JavaScript and Rust.

<Tabs>
  <TabItem label="JavaScript" >

```javascript
import { exists, BaseDirectory } from '@tauri-apps/plugin-fs';
// when using `"withGlobalTauri": true`, you may use
// const { exists, BaseDirectory } = window.__TAURI__.fs;

// Check if the `$APPDATA/avatar.png` file exists
await exists('avatar.png', { baseDir: BaseDirectory.AppData });
```

  </TabItem>
  <TabItem label = "Rust" >

```rust title="src-tauri/src/lib.rs"
use tauri_plugin_fs::FsExt;

#[cfg_attr(mobile, tauri::mobile_entry_point)]
pub fn run() {
  tauri::Builder::default()
      .plugin(tauri_plugin_fs::init())
      .setup(|app| {
          // allowed the given directory
          let scope = app.fs_scope();
        	scope.allow_directory("/path/to/directory", false);
          dbg!(scope.allowed());

          Ok(())
       })
       .run(tauri::generate_context!())
       .expect("error while running tauri application");
}
```

  </TabItem>
</Tabs>

## Security

This module prevents path traversal, not allowing parent directory accessors to be used
(i.e. "/usr/path/to/../file" or "../path/to/file" paths are not allowed).
Paths accessed with this API must be either relative to one of the [base directories][base directory]
or created with the [path API].

See [@tauri-apps/plugin-fs - Security] for more information.

## Paths

The file system plugin offers two ways of manipulating paths: the [base directory] and the [path API].

- base directory

  Every API has an options argument that lets you define a [baseDir][base directory] that acts as the working directory of the operation.

  ```js
  import { readFile } from '@tauri-apps/plugin-fs';
  const contents = await readFile('avatars/tauri.png', {
    baseDir: BaseDirectory.Home,
  });
  ```

  In the above example the ~/avatars/tauri.png file is read since we are using the **Home** base directory.

- path API

  Alternatively you can use the path APIs to perform path manipulations.

  ```js
  import { readFile } from '@tauri-apps/plugin-fs';
  import * as path from '@tauri-apps/api/path';
  const home = await path.homeDir();
  const contents = await readFile(await path.join(home, 'avatars/tauri.png'));
  ```

## Files

### Create

Creates a file and returns a handle to it. If the file already exists, it is truncated.

```js
import { create, BaseDirectory } from '@tauri-apps/plugin-fs';
const file = await create('foo/bar.txt', { baseDir: BaseDirectory.App });
await file.write(new TextEncoder().encode('Hello world'));
await file.close();
```

:::note
Always call `file.close()` when you are done manipulating the file.
:::

### Write

The plugin offers separate APIs for writing text and binary files for performance.

- text files

  ```js
  import { writeTextFile, BaseDirectory } from '@tauri-apps/plugin-fs';
  const contents = JSON.stringify({ notifications: true });
  await writeTextFile('config.json', contents, {
    baseDir: BaseDirectory.AppConfig,
  });
  ```

- binary files

  ```js
  import { writeFile, BaseDirectory } from '@tauri-apps/plugin-fs';
  const contents = new Uint8Array(); // fill a byte array
  await writeFile('config', contents, {
    baseDir: BaseDirectory.AppConfig,
  });
  ```

### Open

Opens a file and returns a handle to it.
With this API you have more control over how the file should be opened
(read-only mode, write-only mode, append instead of overwrite, only create if it does not exist, etc).

:::note
Always call `file.close()` when you are done manipulating the file.
:::

- read-only

  This is the default mode.

  ```js
  import { open, BaseDirectory } from '@tauri-apps/plugin-fs';
  const file = await open('foo/bar.txt', {
    read: true,
    baseDir: BaseDirectory.App,
  });
  const buf = new Uint8Array();
  await file.read(buf);
  const textContents = new TextDecoder().decode(buf);
  await file.close();
  ```

- write-only

  ```js
  import { open, BaseDirectory } from '@tauri-apps/plugin-fs';
  const file = await open('foo/bar.txt', {
    write: true,
    baseDir: BaseDirectory.App,
  });
  await file.write(new TextEncoder().encode('Hello world'));
  await file.close();
  ```

  By default the file is truncated on any `file.write()` call.
  See the following example to learn how to append to the existing contents instead.

- append

  ```js
  import { open, BaseDirectory } from '@tauri-apps/plugin-fs';
  const file = await open('foo/bar.txt', {
    append: true,
    baseDir: BaseDirectory.App,
  });
  await file.write(new TextEncoder().encode('world'));
  await file.close();
  ```

  Note that `{ append: true }` has the same effect as `{ write: true, append: true }`.

- truncate

  When the `truncate` option is set and the file already exists, it will be truncated to length 0.

  ```js
  import { open, BaseDirectory } from '@tauri-apps/plugin-fs';
  const file = await open('foo/bar.txt', {
    write: true,
    truncate: true,
    baseDir: BaseDirectory.App,
  });
  await file.write(new TextEncoder().encode('world'));
  await file.close();
  ```

  This option requires `write` to be `true`.

  You can use it along the `append` option if you want to rewrite an existing file using multiple `file.write()` calls.

- create

  By default the `open` API only opens existing files. To create the file if it does not exist,
  opening it if it does, set `create` to `true`:

  ```js
  import { open, BaseDirectory } from '@tauri-apps/plugin-fs';
  const file = await open('foo/bar.txt', {
    write: true,
    create: true,
    baseDir: BaseDirectory.App,
  });
  await file.write(new TextEncoder().encode('world'));
  await file.close();
  ```

  In order for the file to be created, `write` or `append` must also be set to `true`.

  To fail if the file already exists, see `createNew`.

- createNew

  `createNew` works similarly to `create`, but if the file does not exist, the operation fails.

  ```js
  import { open, BaseDirectory } from '@tauri-apps/plugin-fs';
  const file = await open('foo/bar.txt', {
    write: true,
    createNew: true,
    baseDir: BaseDirectory.App,
  });
  await file.write(new TextEncoder().encode('world'));
  await file.close();
  ```

  In order for the file to be created, `write` must also be set to `true`.

### Read

The plugin offers separate APIs for reading text and binary files for performance.

- text files

  ```js
  import { readTextFile, BaseDirectory } from '@tauri-apps/plugin-fs';
  const configToml = await readTextFile('config.toml', {
    baseDir: BaseDirectory.AppConfig,
  });
  ```

  If the file is large you can stream its lines with the `readTextFileLines` API:

  ```typescript
  import { readTextFileLines, BaseDirectory } from '@tauri-apps/plugin-fs';
  const lines = await readTextFileLines('app.logs', {
    baseDir: BaseDirectory.AppLog,
  });
  for await (const line of lines) {
    console.log(line);
  }
  ```

- binary files

  ```js
  import { readFile, BaseDirectory } from '@tauri-apps/plugin-fs';
  const icon = await readFile('icon.png', {
    baseDir: BaseDirectory.Resources,
  });
  ```

### Remove

Call `remove()` to delete a file. If the file does not exist, an error is returned.

```js
import { remove, BaseDirectory } from '@tauri-apps/plugin-fs';
await remove('user.db', { baseDir: BaseDirectory.AppLocalData });
```

### Copy

The `copyFile` function takes the source and destination paths.
Note that you must configure each base directory separately.

```js
import { copyFile, BaseDirectory } from '@tauri-apps/plugin-fs';
await copyFile('user.db', 'user.db.bk', {
  fromPathBaseDir: BaseDirectory.AppLocalData,
  toPathBaseDir: BaseDirectory.Temp,
});
```

In the above example the \<app-local-data\>/user.db file is copied to $TMPDIR/user.db.bk.

### Exists

Use the `exists()` function to check if a file exists:

```js
import { exists, BaseDirectory } from '@tauri-apps/plugin-fs';
const tokenExists = await exists('token', {
  baseDir: BaseDirectory.AppLocalData,
});
```

### Metadata

File metadata can be retrieved with the `stat` and the `lstat` functions.
`stat` follows symlinks (and returns an error if the actual file it points to is not allowed by the scope)
and `lstat` does not follow symlinks, returning the information of the symlink itself.

```js
import { stat, BaseDirectory } from '@tauri-apps/plugin-fs';
const metadata = await stat('app.db', {
  baseDir: BaseDirectory.AppLocalData,
});
```

### Rename

The `rename` function takes the source and destination paths.
Note that you must configure each base directory separately.

```js
import { rename, BaseDirectory } from '@tauri-apps/plugin-fs';
await rename('user.db.bk', 'user.db', {
  fromPathBaseDir: BaseDirectory.AppLocalData,
  toPathBaseDir: BaseDirectory.Temp,
});
```

In the above example the \<app-local-data\>/user.db.bk file is renamed to $TMPDIR/user.db.

### Truncate

Truncates or extends the specified file to reach the specified file length (defaults to 0).

- truncate to 0 length

```typescript
import { truncate } from '@tauri-apps/plugin-fs';
await truncate('my_file.txt', 0, { baseDir: BaseDirectory.AppLocalData });
```

- truncate to a specific length

```typescript
import {
  truncate,
  readTextFile,
  writeTextFile,
  BaseDirectory,
} from '@tauri-apps/plugin-fs';

const filePath = 'file.txt';
await writeTextFile(filePath, 'Hello World', {
  baseDir: BaseDirectory.AppLocalData,
});
await truncate(filePath, 7, {
  baseDir: BaseDirectory.AppLocalData,
});
const data = await readTextFile(filePath, {
  baseDir: BaseDirectory.AppLocalData,
});
console.log(data); // "Hello W"
```

## Directories

### Create

To create a directory, call the `mkdir` function:

```js
import { mkdir, BaseDirectory } from '@tauri-apps/plugin-fs';
await mkdir('images', {
  baseDir: BaseDirectory.AppLocalData,
});
```

### Read

The `readDir` function recursively lists the entries of a directory:

```typescript
import { readDir, BaseDirectory } from '@tauri-apps/plugin-fs';
const entries = await readDir('users', { baseDir: BaseDirectory.AppLocalData });
```

### Remove

Call `remove()` to delete a directory. If the directory does not exist, an error is returned.

```js
import { remove, BaseDirectory } from '@tauri-apps/plugin-fs';
await remove('images', { baseDir: BaseDirectory.AppLocalData });
```

If the directory is not empty, the `recursive` option must be set to `true`:

```js
import { remove, BaseDirectory } from '@tauri-apps/plugin-fs';
await remove('images', {
  baseDir: BaseDirectory.AppLocalData,
  recursive: true,
});
```

### Exists

Use the `exists()` function to check if a directory exists:

```js
import { exists, BaseDirectory } from '@tauri-apps/plugin-fs';
const tokenExists = await exists('images', {
  baseDir: BaseDirectory.AppLocalData,
});
```

### Metadata

Directory metadata can be retrieved with the `stat` and the `lstat` functions.
`stat` follows symlinks (and returns an error if the actual file it points to is not allowed by the scope)
and `lstat` does not follow symlinks, returning the information of the symlink itself.

```js
import { stat, BaseDirectory } from '@tauri-apps/plugin-fs';
const metadata = await stat('databases', {
  baseDir: BaseDirectory.AppLocalData,
});
```

## Watching changes

To watch a directory or file for changes, use the `watch` or `watchImmediate` functions.

- watch

  `watch` is debounced so it only emits events after a certain delay:

  ```js
  import { watch, BaseDirectory } from '@tauri-apps/plugin-fs';
  await watch(
    'app.log',
    (event) => {
      console.log('app.log event', event);
    },
    {
      baseDir: BaseDirectory.AppLog,
      delayMs: 500,
    }
  );
  ```

- watchImmediate

  `watchImmediate` immediately notifies listeners of an event:

  ```js
  import { watchImmediate, BaseDirectory } from '@tauri-apps/plugin-fs';
  await watchImmediate(
    'logs',
    (event) => {
      console.log('logs directory event', event);
    },
    {
      baseDir: BaseDirectory.AppLog,
      recursive: true,
    }
  );
  ```

By default watch operations on a directory are not recursive.
Set the `recursive` option to `true` to recursively watch for changes on all sub-directories.

:::note
The watch functions require the `watch` feature flag:

```toml title="src-tauri/Cargo.toml"
[dependencies]
tauri-plugin-fs = { version = "2.0.0", features = ["watch"] }
```

:::

## Permissions

By default all potentially dangerous plugin commands and scopes are blocked and cannot be accessed. You must modify the permissions in your `capabilities` configuration to enable these.

See the [Capabilities Overview](/security/capabilities/) for more information and the [step by step guide](/learn/security/using-plugin-permissions/) to use plugin permissions.

```json title="src-tauri/capabilities/default.json" ins={7-11}
{
  "$schema": "../gen/schemas/desktop-schema.json",
  "identifier": "main-capability",
  "description": "Capability for the main window",
  "windows": ["main"],
  "permissions": [
    "fs:default",
    {
      "identifier": "fs:allow-exists",
      "allow": [{ "path": "$APPDATA/*" }]
    }
  ]
}
```

<PluginPermissions plugin={frontmatter.plugin} />

### Scopes

This plugin permissions includes scopes for defining which paths are allowed or explicitly rejected.
For more information on scopes, see the [Command Scopes].

Each `allow` or `deny` scope must include an array listing all paths that should be allowed or denied.
The scope entries are in the `{ path: string }` format.

:::note
`deny` take precedence over `allow` so if a path is denied by a scope, it will be blocked at runtime
even if it is allowed by another scope.
:::

Scope entries can use `$<path>` variables to reference common system paths such as the home directory,
the app resources directory and the config directory. The following table lists all common paths you can reference:

| Path                                                                                            | Variable      |
| ----------------------------------------------------------------------------------------------- | ------------- |
| [appConfigDir](https://v2.tauri.app/reference/javascript/api/namespacepath/#appconfigdir)       | $APPCONFIG    |
| [appDataDir](https://v2.tauri.app/reference/javascript/api/namespacepath/#appdatadir)           | $APPDATA      |
| [appLocalDataDir](https://v2.tauri.app/reference/javascript/api/namespacepath/#appLocaldatadir) | $APPLOCALDATA |
| [appcacheDir](https://v2.tauri.app/reference/javascript/api/namespacepath/#appcachedir)         | $APPCACHE     |
| [applogDir](https://v2.tauri.app/reference/javascript/api/namespacepath/#applogdir)             | $APPLOG       |
| [audioDir](https://v2.tauri.app/reference/javascript/api/namespacepath/#audiodir)               | $AUDIO        |
| [cacheDir](https://v2.tauri.app/reference/javascript/api/namespacepath/#cachedir)               | $CACHE        |
| [configDir](https://v2.tauri.app/reference/javascript/api/namespacepath/#configdir)             | $CONFIG       |
| [dataDir](https://v2.tauri.app/reference/javascript/api/namespacepath/#datadir)                 | $DATA         |
| [localDataDir](https://v2.tauri.app/reference/javascript/api/namespacepath/#localdatadir)       | $LOCALDATA    |
| [desktopDir](https://v2.tauri.app/reference/javascript/api/namespacepath/#desktopdir)           | $DESKTOP      |
| [documentDir](https://v2.tauri.app/reference/javascript/api/namespacepath/#documentdir)         | $DOCUMENT     |
| [downloadDir](https://v2.tauri.app/reference/javascript/api/namespacepath/#downloaddir)         | $DOWNLOAD     |
| [executableDir](https://v2.tauri.app/reference/javascript/api/namespacepath/#executabledir)     | $EXE          |
| [fontDir](https://v2.tauri.app/reference/javascript/api/namespacepath/#fontdir)                 | $FONT         |
| [homeDir](https://v2.tauri.app/reference/javascript/api/namespacepath/#homedir)                 | $HOME         |
| [pictureDir](https://v2.tauri.app/reference/javascript/api/namespacepath/#picturedir)           | $PICTURE      |
| [publicDir](https://v2.tauri.app/reference/javascript/api/namespacepath/#publicdir)             | $PUBLIC       |
| [runtimeDir](https://v2.tauri.app/reference/javascript/api/namespacepath/#runtimedir)           | $RUNTIME      |
| [templateDir](https://v2.tauri.app/reference/javascript/api/namespacepath/#templatedir)         | $TEMPLATE     |
| [videoDir](https://v2.tauri.app/reference/javascript/api/namespacepath/#videodir)               | $VIDEO        |
| [resourceDir](https://v2.tauri.app/reference/javascript/api/namespacepath/#resourcedir)         | $RESOURCE     |
| [tempDir](https://v2.tauri.app/reference/javascript/api/namespacepath/#tempdir)                 | $TEMP         |

#### Examples

- global scope

To apply a scope to any `fs` command, use the `fs:scope` permission:

```json title="src-tauri/capabilities/default.json" {7-10}
{
  "$schema": "../gen/schemas/desktop-schema.json",
  "identifier": "main-capability",
  "description": "Capability for the main window",
  "windows": ["main"],
  "permissions": [
    {
      "identifier": "fs:scope",
      "allow": [{ "path": "$APPDATA" }, { "path": "$APPDATA/**" }]
    }
  ]
}
```

To apply a scope to a specific `fs` command,
use the the object form of permissions `{ "identifier": string, "allow"?: [], "deny"?: [] }`:

```json title="src-tauri/capabilities/default.json" {7-18}
{
  "$schema": "../gen/schemas/desktop-schema.json",
  "identifier": "main-capability",
  "description": "Capability for the main window",
  "windows": ["main"],
  "permissions": [
    {
      "identifier": "fs:allow-rename",
      "allow": [{ "path": "$HOME/**" }]
    },
    {
      "identifier": "fs:allow-rename",
      "deny": [{ "path": "$HOME/.config/**" }]
    },
    {
      "identifier": "fs:allow-exists",
      "allow": [{ "path": "$APPDATA/*" }]
    }
  ]
}
```

In the above example you can use the [`exists`](#exists) API using any `$APPDATA` sub path (does not include sub-directories)
and the [`rename`](#rename)

[NSPrivacyAccessedAPICategoryFileTimestamp]: https://developer.apple.com/documentation/bundleresources/privacy_manifest_files/describing_use_of_required_reason_api#4278393
[C617.1]: https://developer.apple.com/documentation/bundleresources/privacy_manifest_files/describing_use_of_required_reason_api#4278393
[base directory]: /reference/javascript/api/namespacepath/#basedirectory
[path API]: /reference/javascript/api/namespacepath/
[@tauri-apps/plugin-fs - Security]: /reference/javascript/fs/#security
[Command Scopes]: /security/scope/
